﻿Imports System
Imports System.Collections.Generic 
Imports System.Text 
Imports System.Windows.Forms 
Imports System.Windows.Forms.VisualStyles 
Imports System.Drawing 
Imports System.ComponentModel 

''' <summary>
''' Extended Toolstrip control; adds click-through functionality
''' </summary>
''' <remarks>See http://blogs.msdn.com/rickbrew/archive/2006/01/09/511003.aspx</remarks>
Public Class ToolStripEx
    Inherits ToolStrip

    Public Sub New()
        MyBase.New()
    End Sub

    Public Sub New(ByVal items() As ToolStripItem)
        MyBase.New(items)
    End Sub

    Private _ClickThrough As Boolean = True
    Public Property ClickThrough() As Boolean
        Get
            Return (_ClickThrough)
        End Get
        Set(ByVal value As Boolean)
            _ClickThrough = value
        End Set
    End Property

    Private Const WM_MOUSEACTIVATE = &H21
    Private Const MA_ACTIVATE = 1
    Private Const MA_ACTIVATEANDEAT = 2
    Private Const MA_NOACTIVATE = 3
    Private Const MA_NOACTIVATEANDEAT = 4

    ''' <summary>
    ''' Implement click-through functionality so don't have to click inactive form to activate it, then click again to perform action
    ''' </summary>
    ''' <param name="m"></param>
    <System.Diagnostics.DebuggerStepThrough()> _
    Protected Overrides Sub WndProc(ByRef m As System.Windows.Forms.Message)
        MyBase.WndProc(m)
        If _ClickThrough And m.Msg = WM_MOUSEACTIVATE And m.Result = MA_ACTIVATEANDEAT Then m.Result = MA_ACTIVATE
    End Sub

End Class

''' <summary>
''' Extended Menustrip control; adds click-through functionality
''' </summary>
''' <remarks>See http://blogs.msdn.com/rickbrew/archive/2006/01/09/511003.aspx</remarks>
Public Class MenuStripEx
    Inherits MenuStrip

    Private _ClickThrough As Boolean = True
    Public Property ClickThrough() As Boolean
        Get
            Return (_ClickThrough)
        End Get
        Set(ByVal value As Boolean)
            _ClickThrough = value
        End Set
    End Property

    Private Const WM_MOUSEACTIVATE = &H21
    Private Const MA_ACTIVATE = 1
    Private Const MA_ACTIVATEANDEAT = 2
    Private Const MA_NOACTIVATE = 3
    Private Const MA_NOACTIVATEANDEAT = 4

    ''' <summary>
    ''' Implement click-through functionality so don't have to click inactive form to activate it, then click again to perform action
    ''' </summary>
    ''' <param name="m"></param>
    <System.Diagnostics.DebuggerStepThrough()> _
    Protected Overrides Sub WndProc(ByRef m As System.Windows.Forms.Message)
        MyBase.WndProc(m)
        If _ClickThrough And m.Msg = WM_MOUSEACTIVATE And m.Result = MA_ACTIVATEANDEAT Then m.Result = MA_ACTIVATE
    End Sub

End Class

'Original: http://blogs.msdn.com/jfoscoding/articles/491523.aspx 
'Wyatt's fixes: http://wyday.com/blog/csharp-splitbutton 
'http://wyday.com/blog/2007/splitbutton-in-c-passing-the-aic-test/
Public Class SplitButton
    Inherits Button
    Private _state As PushButtonState


    Private Const SplitSectionWidth As Integer = 14

    Private Shared BorderSize As Integer = SystemInformation.Border3DSize.Width * 2
    Private dropDownRectangle As New Rectangle()
    Private m_showSplit As Boolean = False

    Private isSplitMenuVisible As Boolean = False
    Private isMouseOutside As Boolean = False

    Private m_SplitMenuStrip As ContextMenuStrip = Nothing
    Private m_SplitMenu As ContextMenu = Nothing

    Private textFormatFlags As TextFormatFlags = textFormatFlags.[Default]

    Public Sub New()
        If Not IsDesignMode(Me) Then Me.AutoSize = True
    End Sub

#Region "Properties"

    <Browsable(False)> _
    Public Overloads Overrides Property ContextMenuStrip() As ContextMenuStrip
        Get
            Return m_SplitMenuStrip
        End Get
        Set(ByVal value As ContextMenuStrip)
            m_SplitMenuStrip = value
        End Set
    End Property

    '<DefaultValue(Nothing)> _
    Public Property SplitMenu() As ContextMenu
        Get
            Return m_SplitMenu
        End Get
        Set(ByVal value As ContextMenu)
            'remove the event handlers for the old SplitMenu 
            If m_SplitMenu IsNot Nothing Then
                RemoveHandler m_SplitMenu.Popup, AddressOf SplitMenu_Popup
            End If

            'add the event handlers for the new SplitMenu 
            If value IsNot Nothing Then
                ShowSplit = True
                AddHandler value.Popup, AddressOf SplitMenu_Popup
            Else
                ShowSplit = False
            End If

            m_SplitMenu = value
        End Set
    End Property

    '<DefaultValue(Nothing)> _
    Public Property SplitMenuStrip() As ContextMenuStrip
        Get
            Return m_SplitMenuStrip
        End Get
        Set(ByVal value As ContextMenuStrip)
            'remove the event handlers for the old SplitMenuStrip 
            If m_SplitMenuStrip IsNot Nothing Then
                RemoveHandler m_SplitMenuStrip.Closing, AddressOf SplitMenuStrip_Closing
                RemoveHandler m_SplitMenuStrip.Opening, AddressOf SplitMenuStrip_Opening
            End If

            'add the event handlers for the new SplitMenuStrip 
            If value IsNot Nothing Then
                ShowSplit = True
                AddHandler value.Closing, AddressOf SplitMenuStrip_Closing
                AddHandler value.Opening, AddressOf SplitMenuStrip_Opening
            Else
                ShowSplit = False
            End If


            m_SplitMenuStrip = value
        End Set
    End Property

    <DefaultValue(False)> _
    Public WriteOnly Property ShowSplit() As Boolean
        Set(ByVal value As Boolean)
            If value <> m_showSplit Then
                m_showSplit = value
                Invalidate()
                If Me.Parent IsNot Nothing Then
                    Me.Parent.PerformLayout()
                End If
            End If
        End Set
    End Property

    Private Property State() As PushButtonState
        Get
            Return _state
        End Get
        Set(ByVal value As PushButtonState)
            If Not _state.Equals(value) Then
                _state = value
                Invalidate()
            End If
        End Set
    End Property

#End Region

    Protected Overloads Overrides Function IsInputKey(ByVal keyData As Keys) As Boolean
        If keyData.Equals(Keys.Down) AndAlso m_showSplit Then
            Return True
        Else

            Return MyBase.IsInputKey(keyData)
        End If
    End Function

    Protected Overloads Overrides Sub OnGotFocus(ByVal e As System.EventArgs)
        If Not m_showSplit Then
            MyBase.OnGotFocus(e)
            Return
        End If

        If Not State.Equals(PushButtonState.Pressed) AndAlso Not State.Equals(PushButtonState.Disabled) Then
            State = PushButtonState.[Default]
        End If
    End Sub

    Protected Overloads Overrides Sub OnKeyDown(ByVal kevent As KeyEventArgs)
        If m_showSplit Then
            If kevent.KeyCode.Equals(Keys.Down) AndAlso Not isSplitMenuVisible Then
                ShowContextMenuStrip()
            ElseIf kevent.KeyCode.Equals(Keys.Space) AndAlso kevent.Modifiers = Keys.None Then

                State = PushButtonState.Pressed
            End If
        End If

        MyBase.OnKeyDown(kevent)
    End Sub

    Protected Overloads Overrides Sub OnKeyUp(ByVal kevent As KeyEventArgs)
        If kevent.KeyCode.Equals(Keys.Space) Then
            If Control.MouseButtons = MouseButtons.None Then
                State = PushButtonState.Normal
            End If
        ElseIf kevent.KeyCode.Equals(Keys.Apps) Then
            If Control.MouseButtons = MouseButtons.None AndAlso Not isSplitMenuVisible Then
                ShowContextMenuStrip()
            End If
        End If

        MyBase.OnKeyUp(kevent)
    End Sub

    Protected Overloads Overrides Sub OnEnabledChanged(ByVal e As EventArgs)
        If Enabled Then
            State = PushButtonState.Normal
        Else
            State = PushButtonState.Disabled
        End If

        MyBase.OnEnabledChanged(e)
    End Sub

    Protected Overloads Overrides Sub OnLostFocus(ByVal e As EventArgs)
        If Not m_showSplit Then
            MyBase.OnLostFocus(e)
            Return
        End If

        If Not State.Equals(PushButtonState.Pressed) AndAlso Not State.Equals(PushButtonState.Disabled) Then
            State = PushButtonState.Normal
        End If
    End Sub

    Protected Overloads Overrides Sub OnMouseEnter(ByVal e As EventArgs)
        If Not m_showSplit Then
            MyBase.OnMouseEnter(e)
            Return
        End If

        If Not State.Equals(PushButtonState.Pressed) AndAlso Not State.Equals(PushButtonState.Disabled) Then
            isMouseOutside = False
            State = PushButtonState.Hot
        End If

    End Sub

    Protected Overloads Overrides Sub OnMouseLeave(ByVal e As EventArgs)
        If Not m_showSplit Then
            MyBase.OnMouseLeave(e)
            Return
        End If

        '!(State.Equals(PushButtonState.Pressed) && isSplitMenuVisible && ClientRectangle.Contains(PointToClient(Cursor.Position)) ) 

        If Not State.Equals(PushButtonState.Pressed) AndAlso Not State.Equals(PushButtonState.Disabled) Then
            If Focused Then
                State = PushButtonState.[Default]
            Else

                State = PushButtonState.Normal
            End If
        End If
    End Sub

    Protected Overloads Overrides Sub OnMouseMove(ByVal mevent As MouseEventArgs)
        If Not ClientRectangle.Contains(mevent.Location) Then
            If State.Equals(PushButtonState.Pressed) AndAlso Not isSplitMenuVisible Then
                isMouseOutside = True
                State = PushButtonState.Hot
            End If
        ElseIf isMouseOutside Then
            'mouse is now inside the button, but was previously outside 
            isMouseOutside = False
            State = PushButtonState.Pressed
        End If

        MyBase.OnMouseMove(mevent)
    End Sub

    Protected Overloads Overrides Sub OnMouseDown(ByVal e As MouseEventArgs)
        If Not m_showSplit Then
            MyBase.OnMouseDown(e)
            Return
        End If

        If dropDownRectangle.Contains(e.Location) AndAlso Not isSplitMenuVisible AndAlso e.Button = MouseButtons.Left Then
            If State <> PushButtonState.Pressed Then
                ShowContextMenuStrip()
            Else
                State = PushButtonState.Hot
            End If
        Else
            State = PushButtonState.Pressed
        End If
    End Sub

    Protected Overloads Overrides Sub OnMouseUp(ByVal mevent As MouseEventArgs)
        If Not m_showSplit Then
            MyBase.OnMouseUp(mevent)
            Return
        End If

        ' if the right button was released inside the button 
        If mevent.Button = MouseButtons.Right AndAlso ClientRectangle.Contains(mevent.Location) AndAlso Not isSplitMenuVisible Then
            ShowContextMenuStrip()
        ElseIf m_SplitMenuStrip Is Nothing AndAlso m_SplitMenu Is Nothing OrElse Not isSplitMenuVisible Then
            SetButtonDrawState()

            If ClientRectangle.Contains(mevent.Location) AndAlso Not dropDownRectangle.Contains(mevent.Location) Then
                OnClick(New EventArgs)
            End If
        End If
    End Sub

    Protected Overloads Overrides Sub OnPaint(ByVal pevent As PaintEventArgs)
        MyBase.OnPaint(pevent)

        If Not m_showSplit Then
            Return
        End If

        Dim g As Graphics = pevent.Graphics
        Dim bounds As Rectangle = Me.ClientRectangle

        ' draw the button background as according to the current state. 
        If State <> PushButtonState.Pressed AndAlso IsDefault AndAlso Not Application.RenderWithVisualStyles Then
            Dim backgroundBounds As Rectangle = bounds
            backgroundBounds.Inflate(-1, -1)
            ButtonRenderer.DrawButton(g, backgroundBounds, State)

            ' button renderer doesnt draw the black frame when themes are off =( 
            g.DrawRectangle(SystemPens.WindowFrame, 0, 0, bounds.Width - 1, bounds.Height - 1)
        Else
            ButtonRenderer.DrawButton(g, bounds, State)
        End If

        ' calculate the current dropdown rectangle. 
        dropDownRectangle = New Rectangle(bounds.Right - SplitSectionWidth, 0, SplitSectionWidth, bounds.Height)

        Dim internalBorder As Integer = BorderSize
        Dim focusRect As New Rectangle(internalBorder - 1, internalBorder - 1, bounds.Width - dropDownRectangle.Width - internalBorder, bounds.Height - (internalBorder * 2) + 2)

        Dim drawSplitLine As Boolean = (State = PushButtonState.Hot OrElse State = PushButtonState.Pressed OrElse Not Application.RenderWithVisualStyles)


        If RightToLeft = RightToLeft.Yes Then
            dropDownRectangle.X = bounds.Left + 1
            focusRect.X = dropDownRectangle.Right

            If drawSplitLine Then
                ' draw two lines at the edge of the dropdown button 
                g.DrawLine(SystemPens.ButtonShadow, bounds.Left + SplitSectionWidth, BorderSize, bounds.Left + SplitSectionWidth, bounds.Bottom - BorderSize)
                g.DrawLine(SystemPens.ButtonFace, bounds.Left + SplitSectionWidth + 1, BorderSize, bounds.Left + SplitSectionWidth + 1, bounds.Bottom - BorderSize)
            End If
        Else
            If drawSplitLine Then
                ' draw two lines at the edge of the dropdown button 
                g.DrawLine(SystemPens.ButtonShadow, bounds.Right - SplitSectionWidth, BorderSize, bounds.Right - SplitSectionWidth, bounds.Bottom - BorderSize)
                g.DrawLine(SystemPens.ButtonFace, bounds.Right - SplitSectionWidth - 1, BorderSize, bounds.Right - SplitSectionWidth - 1, bounds.Bottom - BorderSize)
            End If
        End If

        ' Draw an arrow in the correct location 
        PaintArrow(g, dropDownRectangle)

        'paint the image and text in the "button" part of the splitButton 
        PaintTextandImage(g, New Rectangle(0, 0, ClientRectangle.Width - SplitSectionWidth, ClientRectangle.Height))

        ' draw the focus rectangle. 
        If State <> PushButtonState.Pressed AndAlso Focused AndAlso ShowFocusCues Then
            ControlPaint.DrawFocusRectangle(g, focusRect)
        End If
    End Sub

    Private Sub PaintTextandImage(ByVal g As Graphics, ByVal bounds As Rectangle)
        ' Figure out where our text and image should go 
        Dim text_rectangle As Rectangle
        Dim image_rectangle As Rectangle

        CalculateButtonTextAndImageLayout(bounds, text_rectangle, image_rectangle)

        'draw the image 
        If Image IsNot Nothing Then
            If Enabled Then
                g.DrawImage(Image, image_rectangle.X, image_rectangle.Y, Image.Width, Image.Height)
            Else
                ControlPaint.DrawImageDisabled(g, Image, image_rectangle.X, image_rectangle.Y, BackColor)
            End If
        End If

        ' If we dont' use mnemonic, set formatFlag to NoPrefix as this will show ampersand. 
        If Not UseMnemonic Then
            textFormatFlags = textFormatFlags Or textFormatFlags.NoPrefix
        ElseIf Not ShowKeyboardCues Then
            textFormatFlags = textFormatFlags Or textFormatFlags.HidePrefix
        End If

        'draw the text 
        If Not String.IsNullOrEmpty(Me.Text) Then
            If Enabled Then
                TextRenderer.DrawText(g, Text, Font, text_rectangle, SystemColors.ControlText, textFormatFlags)
            Else
                ControlPaint.DrawStringDisabled(g, Text, Font, BackColor, text_rectangle, textFormatFlags)
            End If
        End If
    End Sub

    Private Sub PaintArrow(ByVal g As Graphics, ByVal dropDownRect As Rectangle)
        Dim middle As New Point(Convert.ToInt32(dropDownRect.Left + dropDownRect.Width / 2), Convert.ToInt32(dropDownRect.Top + dropDownRect.Height / 2))

        'if the width is odd - favor pushing it over one pixel right. 
        middle.X += (dropDownRect.Width Mod 2)

        Dim arrow As Point() = New Point() {New Point(middle.X - 2, middle.Y - 1), New Point(middle.X + 3, middle.Y - 1), New Point(middle.X, middle.Y + 2)}

        If Enabled Then
            g.FillPolygon(SystemBrushes.ControlText, arrow)
        Else
            g.FillPolygon(SystemBrushes.ButtonShadow, arrow)
        End If
    End Sub

    Public Overloads Overrides Function GetPreferredSize(ByVal proposedSize As Size) As Size
        Dim preferredSize As Size = MyBase.GetPreferredSize(proposedSize)

        'autosize correctly for splitbuttons 
        If m_showSplit Then
            If AutoSize Then
                Return CalculateButtonAutoSize()
            ElseIf Not String.IsNullOrEmpty(Text) AndAlso TextRenderer.MeasureText(Text, Font).Width + SplitSectionWidth > preferredSize.Width Then
                Return preferredSize + New Size(SplitSectionWidth + BorderSize * 2, 0)
            End If
        End If

        Return preferredSize
    End Function

    Private Function CalculateButtonAutoSize() As Size
        Dim ret_size As Size = Size.Empty
        Dim text_size As Size = TextRenderer.MeasureText(Text, Font)
        Dim image_size As Size = Size.Empty
        If Image IsNot Nothing Then image_size = Image.Size

        ' Pad the text size 
        If Text.Length <> 0 Then
            text_size.Height += 4
            text_size.Width += 4
        End If

        Select Case TextImageRelation
            Case TextImageRelation.Overlay
                ret_size.Height = Math.Max(IIf(Text.Length = 0, 0, text_size.Height), image_size.Height)
                ret_size.Width = Math.Max(text_size.Width, image_size.Width)
                Exit Select
            Case TextImageRelation.ImageAboveText, TextImageRelation.TextAboveImage
                ret_size.Height = text_size.Height + image_size.Height
                ret_size.Width = Math.Max(text_size.Width, image_size.Width)
                Exit Select
            Case TextImageRelation.ImageBeforeText, TextImageRelation.TextBeforeImage
                ret_size.Height = Math.Max(text_size.Height, image_size.Height)
                ret_size.Width = text_size.Width + image_size.Width
                Exit Select
        End Select

        ' Pad the result 
        ret_size.Height += (Padding.Vertical + 6)
        ret_size.Width += (Padding.Horizontal + 6)

        'pad the splitButton arrow region 
        If m_showSplit Then
            ret_size.Width += SplitSectionWidth
        End If

        Return ret_size
    End Function

#Region "Button Layout Calculations"

    'The following layout functions were taken from Mono's Windows.Forms 
    'implementation, specifically "ThemeWin32Classic.cs", 
    'then modified to fit the context of this splitButton 

    Private Sub CalculateButtonTextAndImageLayout(ByRef content_rect As Rectangle, ByRef textRectangle As Rectangle, ByRef imageRectangle As Rectangle)
        Dim text_size As Size = TextRenderer.MeasureText(Text, Font, content_rect.Size, textFormatFlags)
        Dim image_size As Size = Size.Empty
        If Image IsNot Nothing Then image_size = Image.Size
        'Dim image_size As Size = IIf(Image Is Nothing, Size.Empty, Image.Size)

        textRectangle = Rectangle.Empty
        imageRectangle = Rectangle.Empty

        Select Case TextImageRelation
            Case TextImageRelation.Overlay
                ' Overlay is easy, text always goes here 
                textRectangle = OverlayObjectRect(content_rect, text_size, TextAlign)
                ' Rectangle.Inflate(content_rect, -4, -4); 
                'Offset on Windows 98 style when button is pressed 
                If _state = PushButtonState.Pressed AndAlso Not Application.RenderWithVisualStyles Then
                    textRectangle.Offset(1, 1)
                End If

                ' Image is dependent on ImageAlign 
                If Image IsNot Nothing Then
                    imageRectangle = OverlayObjectRect(content_rect, image_size, ImageAlign)
                End If

                Exit Select
            Case TextImageRelation.ImageAboveText
                content_rect.Inflate(-4, -4)
                LayoutTextAboveOrBelowImage(content_rect, False, text_size, image_size, textRectangle, imageRectangle)
                Exit Select
            Case TextImageRelation.TextAboveImage
                content_rect.Inflate(-4, -4)
                LayoutTextAboveOrBelowImage(content_rect, True, text_size, image_size, textRectangle, imageRectangle)
                Exit Select
            Case TextImageRelation.ImageBeforeText
                content_rect.Inflate(-4, -4)
                LayoutTextBeforeOrAfterImage(content_rect, False, text_size, image_size, textRectangle, imageRectangle)
                Exit Select
            Case TextImageRelation.TextBeforeImage
                content_rect.Inflate(-4, -4)
                LayoutTextBeforeOrAfterImage(content_rect, True, text_size, image_size, textRectangle, imageRectangle)
                Exit Select
        End Select
    End Sub

    Private Function OverlayObjectRect(ByRef container As Rectangle, ByRef sizeOfObject As Size, ByVal alignment As System.Drawing.ContentAlignment) As Rectangle
        Dim x As Integer, y As Integer

        Select Case alignment
            Case System.Drawing.ContentAlignment.TopLeft
                x = 4
                y = 4
                Exit Select
            Case System.Drawing.ContentAlignment.TopCenter
                x = (container.Width - sizeOfObject.Width) / 2
                y = 4
                Exit Select
            Case System.Drawing.ContentAlignment.TopRight
                x = container.Width - sizeOfObject.Width - 4
                y = 4
                Exit Select
            Case System.Drawing.ContentAlignment.MiddleLeft
                x = 4
                y = (container.Height - sizeOfObject.Height) / 2
                Exit Select
            Case System.Drawing.ContentAlignment.MiddleCenter
                x = (container.Width - sizeOfObject.Width) / 2
                y = (container.Height - sizeOfObject.Height) / 2
                Exit Select
            Case System.Drawing.ContentAlignment.MiddleRight
                x = container.Width - sizeOfObject.Width - 4
                y = (container.Height - sizeOfObject.Height) / 2
                Exit Select
            Case System.Drawing.ContentAlignment.BottomLeft
                x = 4
                y = container.Height - sizeOfObject.Height - 4
                Exit Select
            Case System.Drawing.ContentAlignment.BottomCenter
                x = (container.Width - sizeOfObject.Width) / 2
                y = container.Height - sizeOfObject.Height - 4
                Exit Select
            Case System.Drawing.ContentAlignment.BottomRight
                x = container.Width - sizeOfObject.Width - 4
                y = container.Height - sizeOfObject.Height - 4
                Exit Select
            Case Else
                x = 4
                y = 4
                Exit Select
        End Select

        Return New Rectangle(x, y, sizeOfObject.Width, sizeOfObject.Height)
    End Function

    Private Sub LayoutTextBeforeOrAfterImage(ByVal totalArea As Rectangle, ByVal textFirst As Boolean, ByVal textSize As Size, ByVal imageSize As Size, ByRef textRect As Rectangle, ByRef imageRect As Rectangle)
        Dim element_spacing As Integer = 0
        ' Spacing between the Text and the Image 
        Dim total_width As Integer = textSize.Width + element_spacing + imageSize.Width

        If Not textFirst Then
            element_spacing += 2
        End If

        ' If the text is too big, chop it down to the size we have available to it 
        If total_width > totalArea.Width Then
            textSize.Width = totalArea.Width - element_spacing - imageSize.Width
            total_width = totalArea.Width
        End If

        Dim excess_width As Integer = totalArea.Width - total_width
        Dim offset As Integer = 0

        Dim final_text_rect As Rectangle
        Dim final_image_rect As Rectangle

        Dim h_text As HorizontalAlignment = GetHorizontalAlignment(TextAlign)
        Dim h_image As HorizontalAlignment = GetHorizontalAlignment(ImageAlign)

        If h_image = HorizontalAlignment.Left Then
            offset = 0
        ElseIf h_image = HorizontalAlignment.Right AndAlso h_text = HorizontalAlignment.Right Then
            offset = excess_width
        ElseIf h_image = HorizontalAlignment.Center AndAlso (h_text = HorizontalAlignment.Left OrElse h_text = HorizontalAlignment.Center) Then
            offset += CInt((excess_width / 3))
        Else
            offset += CInt((2 * (excess_width / 3)))
        End If

        If textFirst Then
            final_text_rect = New Rectangle(totalArea.Left + offset, AlignInRectangle(totalArea, textSize, TextAlign).Top, textSize.Width, textSize.Height)
            final_image_rect = New Rectangle(final_text_rect.Right + element_spacing, AlignInRectangle(totalArea, imageSize, ImageAlign).Top, imageSize.Width, imageSize.Height)
        Else
            final_image_rect = New Rectangle(totalArea.Left + offset, AlignInRectangle(totalArea, imageSize, ImageAlign).Top, imageSize.Width, imageSize.Height)
            final_text_rect = New Rectangle(final_image_rect.Right + element_spacing, AlignInRectangle(totalArea, textSize, TextAlign).Top, textSize.Width, textSize.Height)
        End If

        textRect = final_text_rect
        imageRect = final_image_rect
    End Sub

    Private Sub LayoutTextAboveOrBelowImage(ByVal totalArea As Rectangle, ByVal textFirst As Boolean, ByVal textSize As Size, ByVal imageSize As Size, ByRef textRect As Rectangle, ByRef imageRect As Rectangle)
        Dim element_spacing As Integer = 0
        ' Spacing between the Text and the Image 
        Dim total_height As Integer = textSize.Height + element_spacing + imageSize.Height

        If textFirst Then
            element_spacing += 2
        End If

        If textSize.Width > totalArea.Width Then
            textSize.Width = totalArea.Width
        End If

        ' If the there isn't enough room and we're text first, cut out the image 
        If total_height > totalArea.Height AndAlso textFirst Then
            imageSize = Size.Empty
            total_height = totalArea.Height
        End If

        Dim excess_height As Integer = totalArea.Height - total_height
        Dim offset As Integer = 0

        Dim final_text_rect As Rectangle
        Dim final_image_rect As Rectangle

        Dim v_text As VerticalAlignment = GetVerticalAlignment(TextAlign)
        Dim v_image As VerticalAlignment = GetVerticalAlignment(ImageAlign)

        If v_image = VerticalAlignment.Top Then
            offset = 0
        ElseIf v_image = VerticalAlignment.Bottom AndAlso v_text = VerticalAlignment.Bottom Then
            offset = excess_height
        ElseIf v_image = VerticalAlignment.Center AndAlso (v_text = VerticalAlignment.Top OrElse v_text = VerticalAlignment.Center) Then
            offset += CInt((excess_height / 3))
        Else
            offset += CInt((2 * (excess_height / 3)))
        End If

        If textFirst Then
            final_text_rect = New Rectangle(AlignInRectangle(totalArea, textSize, TextAlign).Left, totalArea.Top + offset, textSize.Width, textSize.Height)
            final_image_rect = New Rectangle(AlignInRectangle(totalArea, imageSize, ImageAlign).Left, final_text_rect.Bottom + element_spacing, imageSize.Width, imageSize.Height)
        Else
            final_image_rect = New Rectangle(AlignInRectangle(totalArea, imageSize, ImageAlign).Left, totalArea.Top + offset, imageSize.Width, imageSize.Height)
            final_text_rect = New Rectangle(AlignInRectangle(totalArea, textSize, TextAlign).Left, final_image_rect.Bottom + element_spacing, textSize.Width, textSize.Height)

            If final_text_rect.Bottom > totalArea.Bottom Then
                final_text_rect.Y = totalArea.Top
            End If
        End If

        textRect = final_text_rect
        imageRect = final_image_rect
    End Sub

    Private Function GetHorizontalAlignment(ByVal align As System.Drawing.ContentAlignment) As HorizontalAlignment
        Select Case align
            Case System.Drawing.ContentAlignment.BottomLeft, System.Drawing.ContentAlignment.MiddleLeft, System.Drawing.ContentAlignment.TopLeft
                Return HorizontalAlignment.Left
            Case System.Drawing.ContentAlignment.BottomCenter, System.Drawing.ContentAlignment.MiddleCenter, System.Drawing.ContentAlignment.TopCenter
                Return HorizontalAlignment.Center
            Case System.Drawing.ContentAlignment.BottomRight, System.Drawing.ContentAlignment.MiddleRight, System.Drawing.ContentAlignment.TopRight
                Return HorizontalAlignment.Right
        End Select

        Return HorizontalAlignment.Left
    End Function

    Private Function GetVerticalAlignment(ByVal align As System.Drawing.ContentAlignment) As VerticalAlignment
        Select Case align
            Case System.Drawing.ContentAlignment.TopLeft, System.Drawing.ContentAlignment.TopCenter, System.Drawing.ContentAlignment.TopRight
                Return VerticalAlignment.Top
            Case System.Drawing.ContentAlignment.MiddleLeft, System.Drawing.ContentAlignment.MiddleCenter, System.Drawing.ContentAlignment.MiddleRight
                Return VerticalAlignment.Center
            Case System.Drawing.ContentAlignment.BottomLeft, System.Drawing.ContentAlignment.BottomCenter, System.Drawing.ContentAlignment.BottomRight
                Return VerticalAlignment.Bottom
        End Select

        Return VerticalAlignment.Top
    End Function

    Friend Function AlignInRectangle(ByVal outer As Rectangle, ByVal inner As Size, ByVal align As System.Drawing.ContentAlignment) As Rectangle
        Dim x As Integer = 0
        Dim y As Integer = 0

        If align = System.Drawing.ContentAlignment.BottomLeft OrElse align = System.Drawing.ContentAlignment.MiddleLeft OrElse align = System.Drawing.ContentAlignment.TopLeft Then
            x = outer.X
        ElseIf align = System.Drawing.ContentAlignment.BottomCenter OrElse align = System.Drawing.ContentAlignment.MiddleCenter OrElse align = System.Drawing.ContentAlignment.TopCenter Then
            x = Math.Max(outer.X + ((outer.Width - inner.Width) / 2), outer.Left)
        ElseIf align = System.Drawing.ContentAlignment.BottomRight OrElse align = System.Drawing.ContentAlignment.MiddleRight OrElse align = System.Drawing.ContentAlignment.TopRight Then
            x = outer.Right - inner.Width
        End If
        If align = System.Drawing.ContentAlignment.TopCenter OrElse align = System.Drawing.ContentAlignment.TopLeft OrElse align = System.Drawing.ContentAlignment.TopRight Then
            y = outer.Y
        ElseIf align = System.Drawing.ContentAlignment.MiddleCenter OrElse align = System.Drawing.ContentAlignment.MiddleLeft OrElse align = System.Drawing.ContentAlignment.MiddleRight Then
            y = outer.Y + (outer.Height - inner.Height) / 2
        ElseIf align = System.Drawing.ContentAlignment.BottomCenter OrElse align = System.Drawing.ContentAlignment.BottomRight OrElse align = System.Drawing.ContentAlignment.BottomLeft Then
            y = outer.Bottom - inner.Height
        End If

        Return New Rectangle(x, y, Math.Min(inner.Width, outer.Width), Math.Min(inner.Height, outer.Height))
    End Function

#End Region


    Private Sub ShowContextMenuStrip()
        State = PushButtonState.Pressed

        If m_SplitMenu IsNot Nothing Then
            m_SplitMenu.Show(Me, New Point(0, Height - 1))
        ElseIf m_SplitMenuStrip IsNot Nothing Then
            m_SplitMenuStrip.Show(Me, New Point(0, Height - 1), ToolStripDropDownDirection.BelowRight)
        End If
    End Sub

    Private Sub SplitMenuStrip_Opening(ByVal sender As Object, ByVal e As CancelEventArgs)
        isSplitMenuVisible = True
    End Sub

    Private Sub SplitMenuStrip_Closing(ByVal sender As Object, ByVal e As ToolStripDropDownClosingEventArgs)
        isSplitMenuVisible = False

        'if the user clicks outside the button, set the button state 
        If Not Bounds.Contains(Parent.PointToClient(Cursor.Position)) Then
            SetButtonDrawState()
        End If
    End Sub

    Private Sub SplitMenu_Popup(ByVal sender As Object, ByVal e As EventArgs)
        isSplitMenuVisible = True
    End Sub

    Private Sub SplitMenu_Collapse()
        isSplitMenuVisible = False

        'if the user clicks outside the button, set the button state 
        If Not Bounds.Contains(Parent.PointToClient(Cursor.Position)) Then
            SetButtonDrawState()
        End If
    End Sub

    <System.Diagnostics.DebuggerStepThrough()> _
    Protected Overloads Overrides Sub WndProc(ByRef m As Message)
        If m.Msg = 530 Then
            'this message is only sent when a ContextMenu is closed (not a ContextMenuStrip) 
            SplitMenu_Collapse()
        End If

        MyBase.WndProc(m)
    End Sub

    Private Sub SetButtonDrawState()
        If Bounds.Contains(Parent.PointToClient(Cursor.Position)) Then
            State = PushButtonState.Hot
        ElseIf Focused Then
            State = PushButtonState.[Default]
        ElseIf Not Enabled Then
            State = PushButtonState.Disabled
        Else
            State = PushButtonState.Normal
        End If
    End Sub
End Class

''' <summary>
''' Drop-down combo box automatically filled with non-system colors
''' </summary>
''' <remarks>Adapted from similar control for datagridview column</remarks>
Public Class ColorPickerCombo
    Inherits ComboBox

    Public Sub New()
        MyBase.New()
        If IsDesignMode(Me) Then Exit Sub
        DrawMode = Windows.Forms.DrawMode.OwnerDrawFixed
        'add standard, non-system colors to sorted list
        Sorted = False
        DropDownStyle = ComboBoxStyle.DropDownList
        Items.Clear()
        'Dim SysColors() As String = {"Active", "App", "Button", "Control", "Desktop", "Gradient", "GrayText", "Highlight", "Hot", "Inactive", "Info", "Menu", "Scroll", "Window"}
        'For Each c As String In [Enum].GetNames(GetType(KnownColor))
        '    Dim IsSys As Boolean = False
        '    For Each s As String In SysColors
        '        If c.StartsWith(s) Then IsSys = True : Exit For
        '    Next
        '    If Not IsSys Then Items.Add(c)
        'Next

        'instead of returning colors in alphabetical order, sort by similar colors
        For Each c As Drawing.Color In SortedColors()
            Items.Add(c.Name)
        Next
    End Sub

    ''' <summary>
    ''' Taken from http://www.codeproject.com/KB/selection/MultiTabColorPicker.aspx
    ''' See also: http://groups.google.com/group/microsoft.public.dotnet.framework.windowsforms.controls/browse_thread/thread/6c75aaa3bc23a954/4d6f4c0ff1583e1a?q=group:*dotnet*+%22sort+colors%22#4d6f4c0ff1583e1a
    ''' </summary>
    ''' <remarks></remarks>
    Private Class HSB_ColorComparer
        Implements IComparer(Of Color)

        Public Function Compare(ByVal c1 As Color, ByVal c2 As Color) As Integer Implements IComparer(Of System.Drawing.Color).Compare
            If c1 = Color.Transparent Then
                Return -1
            ElseIf c2 = Color.Transparent Then
                Return 1
            End If
            If c1.GetHue() > c2.GetHue() Then
                Return 1
            ElseIf c1.GetHue() < c2.GetHue() Then
                Return -1
            Else
                If c1.GetSaturation() > c2.GetSaturation() Then
                    Return 1
                ElseIf c1.GetSaturation() < c2.GetSaturation() Then
                    Return -1
                Else
                    If c1.ToArgb() > c2.ToArgb() Then
                        Return 1
                    ElseIf c1.ToArgb() < c2.ToArgb() Then
                        Return -1
                    Else
                        Return 0
                    End If
                End If
            End If
        End Function

    End Class

    ''' <summary>
    ''' Taken from http://www.codeproject.com/KB/selection/MultiTabColorPicker.aspx
    ''' </summary>
    ''' <returns>Sorted arraylist of colors in order of that used in VS</returns>
    ''' <remarks></remarks>
    Private Function SortedColors() As Generic.List(Of Drawing.Color)
        Dim lstColors As New Generic.List(Of System.Drawing.Color)
        Dim colorType As Type = GetType(System.Drawing.Color)
        For Each pi As System.Reflection.PropertyInfo In colorType.GetProperties(Reflection.BindingFlags.Public Or Reflection.BindingFlags.Static)
            If pi.PropertyType.Equals(colorType) Then
                lstColors.Add(CType(pi.GetValue(colorType, Nothing), System.Drawing.Color))
            End If
        Next
        Dim HSB_Comparer As New HSB_ColorComparer
        lstColors.Sort(HSB_Comparer)
        Return lstColors
    End Function

    ''' <summary>
    ''' This prevents the list from being filled twice (once by designer, then again later)
    ''' </summary>
    <DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden), Browsable(False), EditorBrowsable(EditorBrowsableState.Never)> _
    Overloads Property Items()
        Get
            Return MyBase.Items
        End Get
        Set(ByVal value)

        End Set
    End Property

    Protected Overrides Sub OnDrawItem(ByVal e As System.Windows.Forms.DrawItemEventArgs)
        MyBase.OnDrawItem(e)
        If e.Index = -1 Then Exit Sub
        Dim rect As New Rectangle(e.Bounds.X + 1, e.Bounds.Y + 1, e.Bounds.Width - 2, e.Bounds.Height - 2)
        Dim BlockColor As Color = Color.FromName(Items(e.Index))
        DrawColorItem(e.Graphics, rect, BlockColor, Me.Font)
    End Sub

    Friend Shared Sub DrawColorItem(ByVal g As Graphics, ByVal Rect As Rectangle, ByVal ItemColor As Color, ByVal ItemFont As Font)
        'draw rectangle filled with color; on top draw color name text, setting text color as black or white to give good contrast
        Dim sf As New StringFormat
        sf = New StringFormat()
        sf.LineAlignment = StringAlignment.Center
        sf.Alignment = StringAlignment.Center
        g.FillRectangle(New SolidBrush(ItemColor), Rect)
        g.DrawRectangle(Pens.Black, Rect)
        Dim b As Brush = Brushes.Black
        Dim DarkColors() As String = {"Black", "Blue", "Brown", "Crimson", "Dark", "Deep", "Dim", "Dodger", "Forest", "Fuchsia", "Gray", "Green", "Indigo", "Magenta", "Maroon", "Medium", "Midnight", "Navy", "Olive", "Orange", "Purple", "Red", "Rosy", "Royal", "SeaGreen", "Sienna", "Slate", "Steel", "Teal"}
        For Each s As String In DarkColors
            If ItemColor.Name.StartsWith(s) Then b = Brushes.White : Exit For
        Next
        g.DrawString(ItemColor.Name, ItemFont, b, Rect, sf)
        sf.Dispose()
    End Sub
End Class


''' <summary>
''' Specialized version of Listview, intended to show multi-column detail list
''' </summary>
''' <remarks></remarks>
Public Class MyListView
    Inherits ListView

    Public Sub New()
        MyBase.New()
        Activation = ItemActivation.Standard
        CheckBoxes = True
        FullRowSelect = True
        GridLines = True
        HeaderStyle = ColumnHeaderStyle.Nonclickable
        MultiSelect = False
        View = View.Details
        HideSelectedRow = True
    End Sub

    ''' <summary>
    ''' If true, row selection will be hidden for listviews having checkboxes, as this can be confusing (will still have dotted highlight)
    ''' </summary>
    Public Property HideSelectedRow As Boolean

    Public Property CheckOnClick As Boolean

    ''' <summary>
    ''' When check boxes used, don't allow row to appear highlighted
    ''' </summary>
    Private Sub MyListView_SelectedIndexChanged(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.SelectedIndexChanged
        If CheckBoxes Then
            If CheckOnClick AndAlso MyBase.SelectedItems.Count = 1 Then MyBase.SelectedItems(0).Checked = Not MyBase.SelectedItems(0).Checked
            If _HideSelectedRow Then MyBase.SelectedItems.Clear()
        End If
    End Sub

    ''' <summary>
    ''' Initialize control, adding the specified columns left aligned and setting defaults:
    ''' 
    ''' </summary>
    ''' <param name="ColumnNames">Name of column heading</param>
    ''' <remarks></remarks>
    Public Sub AddColumns(ByVal ParamArray ColumnNames() As String)
        Clear()
        For i As Integer = 0 To ColumnNames.Length - 1
            Columns.Add(ColumnNames(i), IIf(i = ColumnNames.Length - 1, -2, -1), HorizontalAlignment.Left)
        Next
    End Sub

    ''' <summary>
    ''' Add row of text items and invisible tag associated with row
    ''' </summary>
    Public Sub AddRow(ByVal Tag As Object, ByVal ParamArray _Items() As String)
        With Items.Add(_Items(0))
            For i As Integer = 1 To _Items.Length - 1
                .SubItems.Add(_Items(i))
            Next
            .Tag = Tag
        End With
    End Sub

    Public Overloads Sub AutoResizeColumns(ByVal ExpandLast As Boolean)
        MyBase.AutoResizeColumns(ColumnHeaderAutoResizeStyle.ColumnContent)
        If ExpandLast And Columns.Count > 0 Then Columns(Columns.Count - 1).Width = -2
    End Sub

    Public Sub CheckAll(ByVal Checked As Boolean)
        If Not CheckBoxes Then Exit Sub
        For i As Integer = 0 To Items.Count - 1
            Items(i).Checked = Checked
        Next
    End Sub
End Class

